#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/3/15 10:55
# @Author  : Wang Yuhang
# @File    : models.py
# @Func    : 数据库模型处理程序

from . import db
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin, AnonymousUserMixin
from . import login_manager
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app, request, url_for
from datetime import datetime
import hashlib
from markdown import markdown
import bleach
from app.exceptions import ValidationError


@login_manager.user_loader  # 加载用户的回调函数
def load_user(user_id):
    return User.query.get(int(user_id))


class Comment(db.Model):
    __tablename__ = 'comments'
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    body_html = db.Column(db.Text)
    disabled = db.Column(db.Boolean)
    author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))

    @staticmethod
    def on_changed_body(target, value, oldvalue, initiator):
        allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',
                        'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',
                        'h1', 'h2', 'h3', 'p']
        target.body_html = bleach.linkify(bleach.clean(
            markdown(value, output_format='html'),
            tags=allowed_tags, strip=True))


class Follow(db.Model):
    __tablename__ = 'follows'
    follower_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)  # 关注者的id
    followed_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)  # 被关注者的id
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)                       # 关注的时间


class Permission:               # 不同的权限
    FOLLOW = 0x01               # 关注其他用户
    COMMENT = 0x02              # 可在文章中发布评论
    WRITE_ARTICLES = 0x04       # 写文章
    MODERATE_COMMENTS = 0x08    # 管理他人的评论
    ADMINISTER = 0x80           # 管理网站


class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    default = db.Column(db.Boolean, default=False, index=True)
    permissions = db.Column(db.Integer)
    users = db.relationship('User', backref='role', lazy='dynamic')

    def __repr__(self):
        return '<Role %r>' % self.name

    @staticmethod
    def insert_roles():
        roles = {                                                   # 具有不同权限组合的用户
            'User': (Permission.FOLLOW |
                     Permission.COMMENT |
                     Permission.WRITE_ARTICLES, True),             # 默认角色是User
            'Moderator': (Permission.FOLLOW |
                          Permission.COMMENT |
                          Permission.WRITE_ARTICLES |
                          Permission.MODERATE_COMMENTS, False),
            'Administrator': (0xff, False)
        }
        for r in roles:
            role = Role.query.filter_by(name=r).first()
            if role is None:
                role = Role(name=r)
            role.permissions = roles[r][0]
            role.default = roles[r][1]
            db.session.add(role)
        db.session.commit()


class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)                                # id
    email = db.Column(db.String(64), unique=True, index=True)                   # 邮箱
    username = db.Column(db.String(64), unique=True, index=True)                # 用户名
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))                  # 角色id
    password_hash = db.Column(db.String(128))                                   # 密码的哈希值
    confirmed = db.Column(db.Boolean, default=False)                            # 是否验证
    name = db.Column(db.String(64))                                             # 姓名
    location = db.Column(db.String(64))                                         # 位置
    about_me = db.Column(db.Text())                                             # 自我介绍
    member_since = db.Column(db.DateTime(), default=datetime.utcnow)            # 注册日期
    last_seen = db.Column(db.DateTime(), default=datetime.utcnow)               # 最后访问日期
    avatar_hash = db.Column(db.String(32))                                      # 头像URL中用到的hash值
    posts = db.relationship('Post', backref='author', lazy='dynamic')           # 文章内容
    followed = db.relationship('Follow',                                        # 关注的用户
                               foreign_keys=[Follow.follower_id],
                               backref=db.backref('follower', lazy='joined'),
                               lazy='dynamic',
                               cascade='all,delete-orphan')
    followers = db.relationship('Follow',                                       # 粉丝
                                foreign_keys=[Follow.followed_id],
                                backref=db.backref('followed', lazy='joined'),
                                lazy='dynamic',
                                cascade='all,delete-orphan')
    comments = db.relationship('Comment', backref='author', lazy='dynamic')     # 评论

    @staticmethod
    def add_self_follows():  # 用户自己关注自己的方法
        for user in User.query.all():
            if not user.is_following(user):
                user.follow(user)
                db.session.add(user)
                db.session.commit()

    def __init__(self, **kwargs):
        super(User, self).__init__(**kwargs)                                    # 调用基类构造函数
        if self.role is None:
            if self.email == current_app.config['FLASKY_ADMIN']:                # 管理员
                self.role = Role.query.filter_by(permissions=0xff).first()
            if self.role is None:
                self.role = Role.query.filter_by(default=True).first()          # 用户
        if self.email is not None and self.avatar_hash is None:
            self.avatar_hash = hashlib.md5(self.email.encode('utf-8')).hexdigest()
        self.follow(self)                                                       # 自己关注自己

    @property  # 定义为方法，调用时无需使用()
    def followed_posts(self):
        """
        获取所关注用户的文章
        :return:
        """
        return Post.query.join(Follow, Follow.followed_id == Post.author_id).filter(Follow.follower_id == self.id)

    def follow(self, user):
        """
        自己关注某个用户
        :param user:
        :return:
        """
        if not self.is_following(user):                 # 先看是不是已经关注了，没关注的话
            f = Follow(follower=self, followed=user)    # 自己是关注者，user是被关注者
            db.session.add(f)

    def unfollow(self, user):
        """
        自己取消关注某个用户
        :param user:
        :return:
        """
        f = self.followed.filter_by(followed_id=user.id).first()  # 看看自己的followed中是否有user
        if f:
            db.session.delete(f)                                  # 有的话，删除

    def is_following(self, user):
        """
        检查自己是否已经关注了某个用户
        :param user:
        :return:
        """
        if user.id is None:
            return False
        return self.followed.filter_by(followed_id=user.id).first() is not None  # 查询用户id是否在followed_id中

    def is_followed_by(self, user):
        """
        检查自己是否已经被某个用户关注
        :param user:
        :return:
        """
        if user.id is None:
            return False
        return self.followers.filter_by(follower_id=user.id).first() is not None  # 看自己的关注者列表中是否有user

    @property  # 修饰方法，让方法可以像属性一样被访问
    def password(self):  # 试图读取原来的密码时，密码的散列值不可读
        raise AttributeError('密码不可读！')

    @password.setter  # 使password属性在存入前进行预处理
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

    def generate_confirmation_token(self, expiration=3600):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'confirm': self.id})

    def confirm(self, token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except:
            return False
        if data.get('confirm') != self.id:
            return False
        self.confirmed = True
        db.session.add(self)
        return True

    def __repr__(self):
        return '<User %r>' % self.username

    def can(self, permissions):  # 请求角色和赋予角色进行位与操作
        return self.role is not None and \
               (self.role.permissions & permissions) == permissions

    def is_administrator(self):  # 检查管理员角色
        return self.can(Permission.ADMINISTER)

    def ping(self):
        """
        刷新用户的最后访问时间
        :return:
        """
        self.last_seen = datetime.utcnow()
        db.session.add(self)

    def gravatar(self, size=100, default='identicon', rating='g'):
        if request.is_secure:
            url = 'https://gravatar.loli.net/avatar/'  # gravatar镜像
        else:
            url = 'http://gravatar.loli.net/avatar'
        hash = self.avatar_hash or hashlib.md5(self.email.encode('utf-8')).hexdigest()
        return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(url=url, hash=hash,
                                                                     size=size, default=default, rating=rating)

    def generate_auth_token(self, expiration):
        s = Serializer(current_app.config['SECRET_KEY'],
                       expires_in=expiration)
        return s.dumps({'id': self.id}).decode('utf-8')

    @staticmethod
    def verify_auth_token(token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except:
            return None
        return User.query.get(data['id'])

    def to_json(self):
        json_user = {
            'url': url_for('api.get_user', id=self.id),
            'username': self.username,
            'member_since': self.member_since,
            'last_seen': self.last_seen,
            'posts_url': url_for('api.get_user_posts', id=self.id),
            'followed_posts_url': url_for('api.get_user_followed_posts',
                                          id=self.id),
            'post_count': self.posts.count()
        }
        return json_user
    # def change_email(self, token):
    #     ...


class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    body_html = db.Column(db.Text)
    comments = db.relationship('Comment', backref='post', lazy='dynamic')

    @staticmethod
    def on_changed_body(target, value, oldvalue, initiator):
        """
        把body字段中的文本渲染成HTML格式，保存到body_html中
        :param target:
        :param value:
        :param oldvalue:
        :param initiator:
        :return:
        """
        allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',
                        'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',
                        'h1', 'h2', 'h3', 'p']
        target.body_html = bleach.linkify(bleach.clean(  # markdown转换成html
            markdown(value, output_format='html'),       # clean函数删除不在白名单中的标签
            tags=allowed_tags, strip=True))              # linkfy将纯文本的URL转换成<a>链接

    def to_json(self):
        json_post = {
            'url': url_for('api.get_post', id=self.id),
            'body': self.body,
            'body_html': self.body_html,
            'timestamp': self.timestamp,
            'author_url': url_for('api.get_user', id=self.author_id),
            'comments_url': url_for('api.get_post_comments', id=self.id),
            'comment_count': self.comments.count()
        }
        return json_post

    @staticmethod
    def from_json(json_post):
        body = json_post.get('body')
        if body is None or body == '':
            raise ValidationError('post does not have a body')
        return Post(body=body)


class AnonymousUser(AnonymousUserMixin):  # 用户未登录时current_user的值
    def can(self, permissions):
        return False

    def is_administrator(self):
        return False


login_manager.anonymous_user = AnonymousUser
# SQLAlchemy“set”事件的监听程序，只要body字段设置了新值，函数就会自动被调用
db.event.listen(Post.body, 'set', Post.on_changed_body)
db.event.listen(Comment.body, 'set', Comment.on_changed_body)



